using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace Manoso.Data.FluentMap
{
    internal interface IMap
    {
        /// <summary>
        /// Relation defined for the map.
        /// </summary>
        IRelation Relation { get; }

        Type ValueType { get; set; }
        object Value { get; set; }
        object SubValue { get; set; }
        IMap Parent { get; set; }

        IList<IMap> TableMaps { get; set; }
        IMapTable MapTable { get; set; }

        IList<IMap> CommandMaps { get; set; }
        IList<IMapCommand> Commands { get; }

        IList<IMap> QueryMaps { get; set; }
        IList<IMapQuery> Queries { get; set; }

        IEnumerable<object> GetValues(object parent);
        void SetValue(IMapQuery query, Action<object, IEnumerable<IMapProperty>> read);
        void Cast<T, TCast>(Action<IMapSql<TCast>> config = null);
    }

    internal abstract class Map : IMap
    {
        public virtual IRelation Relation { get; internal set; }

        public Type ValueType { get; set; }
        public object Value { get; set; }
        public object SubValue { get; set; }
        public IMap Parent { get; set; }

        public IList<IMap> TableMaps { get; set; }
        public IMapTable MapTable { get; set; }

        public IList<IMap> CommandMaps { get; set; }

        public IList<IMapCommand> Commands
        {
            get { return Deletes.Union(Inserts).Union(Updates).Union(Declares).Union(InsertsX).Union(UpdatesX).ToList(); }
        }

        protected IList<IMapCommand> Deletes { get; set; }
        protected IList<IMapCommand> Inserts { get; set; }
        protected IList<IMapCommand> Updates { get; set; }
        protected IList<IMapCommand> Declares { get; set; }

        protected IList<IMapCommand> InsertsX { get; set; }
        protected IList<IMapCommand> UpdatesX { get; set; }

        public IList<IMap> QueryMaps { get; set; }
        public IList<IMapQuery> Queries { get; set; }

        public static IMap<TP> Create<T, TP>(IRelation relation, IMap parent)
        {
            var map = new Map<T, TP>
            {
                Relation = relation,
                Parent = parent,
                QueryMaps = parent.QueryMaps,
            };

            parent.TableMaps.Add(map);
            parent.CommandMaps.Add(map);
            parent.QueryMaps.Add(map);
            return map;
        }

        public IEnumerable<object> GetValues(object parent)
        {
            if (Parent != null && Parent.ValueType != null && parent.GetType() != Parent.ValueType ||
                parent == null) return null;
            return
                Relation.GetChildren(
                    Parent == null || Parent.ValueType == null ? parent.GetType() : Parent.ValueType, parent);
        }

        public void SetValue(IMapQuery query, Action<object, IEnumerable<IMapProperty>> read)
        {
            if (Value == null) Value = Parent.SubValue;

            var sub = SubValue = query.SetValue(Value, read);

            if (sub == null)
                return;

            if (query.LinkActions != null && query.LinkActions.Count > 0)
                Link(query.LinkActions, sub, false);

            if (query.SelfActions != null && query.SelfActions.Count > 0)
                Link(query.SelfActions, sub, true);
        }

        private void Link(Dictionary<string, Action<object, object>> actions, object sub, bool isSelf)
        {
            foreach (var link in actions)
            {
                var value = GetValue(link.Key, isSelf);
                if (value == null) continue;

                link.Value(sub, value);
            }
        }

        private object GetValue(string key, bool isSelf)
        {
            var map = this;
            while (map != null && map.MapTable.Alias != key)
            {
                map = map.Parent as Map;
            }

            return map == null ? null : (isSelf ? map.Value : map.SubValue);
        }

        public void Cast<T, TCast>(Action<IMapSql<TCast>> config = null)
        {
            var table = new MapTable<T, TCast>
            {
                Relation = Relation,
                ValueType = typeof(TCast),
                TypeTables = MapTable.TypeTables,
                NameProperties = MapTable.NameProperties,
                Name = MapTable.Name,
                Alias = MapTable.Alias
            };

            if (config != null)
                config(table);
            table.Setup();

            MapTable.Derives.Add(typeof(TCast), table);
        }
    }

    public interface IMap<T>
    {
        /// <summary>
        /// Define a id/key property for the current type.
        /// </summary>
        /// <typeparam name="TId">Type of the id property.</typeparam>
        /// <param name="idLambda">Lambda expression define the id property of the current type.</param>
        /// <returns></returns>
        void Pin<TId>(Expression<Func<T, TId>> idLambda);

        /// <summary>
        /// Define the table for type TP.
        /// </summary>
        /// <param name="config">The action to config the table.</param>
        /// <returns></returns>
        void Table(Action<IMapTable<T>> config = null);

        /// <summary>
        /// Used for grouping, create TP object using next lower level node table's columns instead.
        /// </summary>
        /// <param name="config">The action to config the next lower table.</param>
        void View(Action<IMapTable<T>> config = null);

        /// <summary>
        /// Define an extend table for type TP.
        /// </summary>
        /// <param name="config">The action to config the table.</param>
        /// <returns></returns>
        void Extend(Action<IMapTable<T>> config = null);

        /// <summary>
        /// Declare ids of current type for types of on lower levels in the fluent object hierarchy to reference.
        /// </summary>
        /// <param name="config">The action to config the Declare command.</param>
        /// <returns>Current map.</returns>
        void Declare(Action<IMapDeclare<T>> config = null);

        /// <summary>
        /// Define how TP objects should be deleted from DB.
        /// </summary>
        /// <param name="config">The action to config the Delete command.</param>
        /// <returns>Current map.</returns>
        void Delete(Action<IMapDelete<T>> config = null);

        /// <summary>
        /// Define how TP objects should be updated to DB.
        /// </summary>
        /// <param name="config">The action to config the Update command.</param>
        /// <returns>Current map.</returns>
        void Update(Action<IMapUpdate<T>> config = null);

        /// <summary>
        /// Define how TP objects should be inserted to DB.
        /// </summary>
        /// <param name="config">The action to config the Insert command.</param>
        /// <returns>Current map.</returns>
        void Insert(Action<IMapInsert<T>> config = null);

        /// <summary>
        /// Define the query to retrieve TP objects from DB.
        /// </summary>
        /// <param name="config">The action to config the Query.</param>
        /// <returns>Current map.</returns>
        void Query(Action<IMapQuery<T>> config = null);
    }
    
    internal class Map<T, TP> : Map, IMap<TP>
    {
        public Map()
        {
            ValueType = typeof (TP);
            TableMaps = new List<IMap>();
            CommandMaps = new List<IMap>();
            Queries = new List<IMapQuery>();
            Deletes = new List<IMapCommand>();
            Inserts = new List<IMapCommand>(); 
            Updates = new List<IMapCommand>();
            Declares = new List<IMapCommand>();
            InsertsX = new List<IMapCommand>();
            UpdatesX = new List<IMapCommand>();

        }

        public void Pin<TId>(Expression<Func<TP, TId>> idLambda)
        {
            ((IRelation<T, TP>)Relation).Key(idLambda);
        }
        
        public void Table(Action<IMapTable<TP>> config = null)
        {
            if (MapTable != null)
                Queries.Add(MapTable);

            var table = new MapTable<T, TP>
            {
                Relation = Relation,
                ValueType = typeof (TP),
                TypeTables = Parent == null ? new Dictionary<string, IMapTable>() : Parent.MapTable.TypeTables,
                NameProperties = Parent == null ? new Dictionary<string, IMapProperty>() : Parent.MapTable.NameProperties,
            };

            if (config != null)
                config(table);

            table.Setup();
            Queries.Add(table);

            MapTable = table;
        }

        public void View(Action<IMapTable<TP>> config = null)
        {
            var table = new MapTable<T, TP>
            {
                Relation = Relation,
                ValueType = typeof(TP),
                TypeTables = Parent.MapTable.TypeTables,
                NameProperties = Parent.MapTable.NameProperties,
            };

            if (config != null)
                config(table);

            Queries.Add(table);

            MapTable = table;
        }

        public void Extend(Action<IMapTable<TP>> config = null)
        {
            var relation = new Relation<TP, TP>();
            var map = Create<TP, TP>(relation, this);
            map.Table(config);
        }

        public void Declare(Action<IMapDeclare<TP>> config = null)
        {
            var command = new MapDeclare<TP>
            {
                Relation = Relation,
                ValueType = ValueType,
                Table = MapTable.Name,
                Target = MapTable.Name,
                Alias = MapTable.Alias,
            };

            if (config != null)
                config(command);

            Declares.Add(command);
        }

        public void Delete(Action<IMapDelete<TP>> config = null)
        {
            var command = new MapDelete<TP>
            {
                Relation = Relation,
                ValueType = ValueType,
                Table = MapTable.Name,
                Target = MapTable.Name,
                Alias = MapTable.Alias,
                IsTable = true,
            };

            if (config != null)
                config(command);

            if (command.IsExtension)
                Deletes.Insert(0, command);
            else
                Deletes.Add(command);
        }

        public void Update(Action<IMapUpdate<TP>> config = null)
        {
            var command = new MapUpdate<TP>
            {
                Relation = Relation,
                ValueType = ValueType,
                Table = MapTable.Name,
                Target = MapTable.Name,
                Alias = MapTable.Alias,
                IsTable = true,
            };
            Updates.Add(command);

            if (config != null)
                config(command);

            if (command.IsExtension)
                UpdatesX.Add(command);
            else
                Updates.Add(command);
        }

        public void Insert(Action<IMapInsert<TP>> config = null)
        {
            var command = new MapInsert<TP>
            {
                Relation = Relation,
                ValueType = ValueType,
                Table = MapTable.Name,
                Target = MapTable.Name,
                Alias = MapTable.Alias,
                IsTable = true,
            };

            if (config != null)
                config(command);
            
            if (command.IsExtension)
                InsertsX.Add(command);
            else
                Inserts.Add(command);
        }

        public void Query(Action<IMapQuery<TP>> config = null)
        {
            var query = new MapQuery<T, TP>
            {
                Relation = Relation,
                ValueType = ValueType,
                Table = MapTable == null ? string.Empty : MapTable.Name,
                Alias = MapTable == null ? string.Empty : MapTable.Alias,
            };

            if (config != null)
                config(query);

            query.Setup();
            Queries.Add(query);
        }
    }
}